﻿/*
AwpAdmin, an admin tool for Battlefield 3
Copyright (C) 2011 agentwite, Timi, Unseen, AlCapwn

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

You can contact us at http://bf3admin.codeplex.com/.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CSharp;
using Microsoft.VisualBasic.CompilerServices;
using System.Reflection;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.IO;
using System.Security.Cryptography;
using Microsoft.VisualBasic.FileIO;//easy way to copy a dir
using System.Globalization;
using System.Xml.Linq;

namespace AwpAdminTool
{
    /// <summary>
    /// Manages the plugins of an <see cref="AwpAdmin"/>.
    /// </summary>
    public class PluginManager : MarshalByRefObject
    {
        static CultureInfo c = CultureInfo.GetCultureInfo("en-US");

        AwpAdmin Parent;
        internal Dictionary<string, Plugin> Plugins = new Dictionary<string, Plugin>(StringComparer.OrdinalIgnoreCase);//key is folder name
        XElement PluginSettings;

        internal PluginManager(AwpAdmin awpAdmin)
        {
            Parent = awpAdmin;
            PluginSettings = awpAdmin.MainSettings.Element(AwpAdmin.XmlNamespace + "plugins");

            //clear out tempassemblies. delete the entire dir because it's recreated anyway
            if (Directory.Exists(Parent.PluginDirectory + "TempAssemblies\\"))
                Directory.Delete(Parent.PluginDirectory + "TempAssemblies\\", true);

            //load enabled plugins. since this is ctor, dont worry about xelement being modified
            foreach (var p in PluginSettings.Elements(AwpAdmin.XmlNamespace + "plugin"))
                if (PluginExists(p.Value)) LoadPlugin(p.Value);
        }

        /// <summary>
        /// Determine whether or not the plugin folder exists.
        /// </summary>
        /// <param name="folderName">The name of the folder.</param>
        /// <returns>True if that plugin exists.</returns>
        public bool PluginExists(string folderName)
        {
            //do it this way to prevent directory traversal
            foreach (var d in Directory.EnumerateDirectories(Parent.PluginDirectory))
                if (Path.GetFileName(d) == folderName) return true; //use filename because d won't have trailing slash
            return false;
        }

        /// <summary>
        /// Loads a plugin.
        /// </summary>
        /// <param name="folderName">The name of the folder where the plugin is located.</param>
        /// <remarks>
        /// If the plugin is an assembly (.dll), it must be named the same as the folder, such as my_plugin.dll.<para />
        /// If the plugin is code, only .csproj and .vbproj files are supported.
        /// </remarks>
        public void LoadPlugin(string folderName)
        {
            if (Plugins.ContainsKey(folderName)) //reload plugin
            {
                //TODO: reload
            }
            else if (PluginExists(folderName)) //check is here to prevent directory traversal
            {
                string dir = Parent.PluginDirectory + folderName + "\\";
                if (File.Exists(dir + folderName + ".dll")) //assembly plugin
                {
                    //write loading code here because loading is only done in this method

                    string assemblyFile = dir + folderName + ".dll";
                    
                    //first, copy folder to temporary folder based on dll's md5

                    //get new path containing assembly md5
                    MD5 md5 = MD5.Create();
                    FileStream fs = new FileStream(assemblyFile, FileMode.Open, FileAccess.Read);
                    string tempFolder = Parent.PluginDirectory + "TempAssemblies\\" +
                        md5.ComputeHash(fs).ToHexString() + "\\";

                    //dispose md5 and fs
                    md5.Dispose();
                    fs.Dispose();

                    try //put in try block because some files may be read-only and will cause exceptions
                    {
                        //copy folder contents
                        //dont check to see if folder exists because files other than the assembly may have been updated
                        FileSystem.CopyDirectory(dir, tempFolder, true);
                    }
                    catch (UnauthorizedAccessException)
                    {
                        //do nothing
                    }

                    //ensure the assembly exists in the folder
                    string tempFile = tempFolder + folderName + ".dll";
                    if (File.Exists(tempFile))
                    {
                        //finally, load that assembly
                        AppDomain pluginAppDomain = AppDomain.CreateDomain(tempFile);

                        try
                        {
                            Plugin newPlugin = 
                                pluginAppDomain.CreateInstanceFromAndUnwrap(tempFile, "AdminPlugin.PluginMain", false,
                                BindingFlags.CreateInstance, null, new object[] { Parent }, null, null) as Plugin;

                            //add to dict
                            Plugins[folderName] = newPlugin;
                        }
                        catch (Exception) //catch all exceptions
                        {
                            //TODO: add events and catch some specific exceptions. Other exceptions would then be caused by ctor
                            return;
                        }

                        //if it got this far, then plugin loaded just fine.

                        //add to xml
                        PluginSettings.Add(new XElement(AwpAdmin.XmlNamespace + "plugin", folderName));

                        //TODO: event
                    }
                    else throw new FileNotFoundException(
                        string.Format(c, "The newly copied assembly for plugin {0} cannot be found.", folderName));

                }
                //TODO: add code for other plugins
            }
            else throw new ArgumentException("folderName does not exist.", "folderName");
        }

        /// <summary>
        /// Unloads a <see cref="Plugin"/>.
        /// </summary>
        /// <param name="folderName">The folder name where the plugin is located.</param>
        public void UnloadPlugin(string folderName)
        {
            if (Plugins.ContainsKey(folderName))
            {
                //TODO: event
                AppDomain pluginDomain = Plugins[folderName].PluginAppDomain;
                Plugins.Remove(folderName);
                AppDomain.Unload(pluginDomain);
            }
        }
    }
}
